/*
 * Written by Anders Fongen
 * Norwegian Defence Research Establishment (FFI)
 * Email: anders@fongen.no
 */

package no.ffi.gismoidm.idp;

import no.ffi.gismoidm.certstore.CertificateService;
import no.ffi.gismoidm.utils.GismoIdmException;
import no.ffi.gismoidm.utils.IdentityStatement;
import java.io.Serializable;
import java.math.BigInteger;
import java.security.PublicKey;
import java.security.cert.X509Certificate;
import java.util.List;
import javax.security.auth.x500.X500Principal;
import no.ffi.gismoidm.certstore.CertificateServiceFactory;
import no.ffi.tpm.LegacyKeyCertifyInfo;
import no.ffi.tpm.TPMiSRequest;
import no.ffi.tpm.crypto.CryptoOperations;

/**
 * @author anf
 * Created on 18 jan. 2011
 * 
 * Java IdentityStatement Issuer. This class generates IdentityStatements for authentication in
 * a Holder-Of-Key use case. The IdentityStatements includes the name of the subject,
 * and a number of attributes.
 * 
 * The X509 certificate of the subject is validated in the issuing process, 
 * so the certificate does not need validation for a little while. The
 * identityStatement is given a "validity" condition, during which interval the
 * certificate is considered as "trusted". After expiration, the identity statement
 * should not be used.
 * 
 * The IdentityStatement is signed by the issuer, whose public key is by definition
 * trusted (is a trust anchor) by everyone. Therefore, the issuer's certificate
 * is not included in the signature, only its public key.
 */
public class IdentityStatementIssuer {
    

    private static int validityStart;
    private static int validityEnd;
    
    private static IdentityStatementIssuer isi = null;
    
    private static AttributeDAO attrDao;
    private static CryptoOperations issuerCrOp;
    private static CertificateService certService;
    
    
    // Private constructor. Clients should use the getInstance() method
    private IdentityStatementIssuer(CryptoOperations issCrOp) throws Exception {
        issuerCrOp = issCrOp;
        validityStart = Integer.parseInt(Config.getProperty("IDENTITYSTATEMENT_VALIDITY_START", "-60"));
        validityEnd = Integer.parseInt(Config.getProperty("IDENTITYSTATEMENT_VALIDITY_END", "240"));
        
        // Connect to dataabase with attribute values
        attrDao = new AttributeDAO();
        
        // Connect to sertificate service
        certService = CertificateServiceFactory.getInstance("WEBCA",issuerCrOp.getTrustAnchorCert());
    }
    
    /**
     * The use of a getInstance() method supports the singleton pattern
     * 
     * @return Singleton instance of IdentityStatementIssuer
     */
    public static synchronized IdentityStatementIssuer getInstance(CryptoOperations crOp) throws Exception {
        if (isi==null) isi = new IdentityStatementIssuer(crOp);
        return isi;
    }

    // Returns either the object, or the exception object if thrown by underlying code
    public Serializable issueLocalIdentityStatement(String subjectName) {
        try {
            // First, find a validated certificate. The following call
            // may throw several exceptions. "SubjectNotFound", "CertificateNotValidated" etc.
            X509Certificate subjectCert = getValidatedCertificate(new X500Principal(subjectName));

            // Get the attributes from a database
            String subjectId = IdentityStatement.getSubjectAltName(subjectCert);
            List dbAttributes = attrDao.getAttributesFor(subjectId);

            // Create an identity statement
            IdentityStatement fass = new IdentityStatement(subjectCert,//issuerCert,
                dbAttributes, issuerCrOp,validityStart,validityEnd);
            return fass;
        } catch (Exception e) {
            return e;
        }
    }

    
    public Serializable issueCrossDomainJavaIdentityStatement(IdentityStatement localAss) {
        IdentityStatement crossCOIidentStatement=null;
        try {
            // We need the cross identity statement issued from the issuer of the local identity statement
            X500Principal issuerDN = localAss.getIssuerDN();
            XCOIIdentityStatementCache xdc = XCOIIdentityStatementCache.getInstance(issuerCrOp);
            try { // Exception during server-to-server invocation should be reported explicitly
                crossCOIidentStatement = xdc.xdIdentityStatementFrom(issuerDN);
                isIdentityStatementTrusted(localAss); // Throws exception if not
            } catch (Exception e) {
                throw new GismoIdmException("Error during cross-IS issuance:"+e.getMessage());
            }
            localAss.resign(issuerCrOp,crossCOIidentStatement);
            return localAss;
        } catch (Exception e) {
            return e;
        }
    }
    
    // An LK attestation request from a TPM-equipped computer.
    public Serializable issueTPMIdentityStatement(TPMiSRequest request) {
        try {
            byte[] pcrHash = request.getPCRHash();
            System.out.println("LKCert: PCR Hash Values are " + CryptoOperations.toHexString(pcrHash));
            BigInteger pcrInt = new BigInteger(pcrHash);
            //The hash of the subjectID from the user certificate
            String subjectDN=request.getSubjectId(); // The subject DN (not the TPM)
            //The default nonce used for this request
            byte[] nonce = new byte[20];
            //The object containing the LK signed by the AIK
            LegacyKeyCertifyInfo keyInfo= request.getTpmSignedPart();
            //The serial number fo the AIK used for signing
            String aikSnr=keyInfo.getAikSNR();
            // Split the serial no into aik subject hash and aik certificate serial number
            String aikSubjectHash = aikSnr.substring(0,8); // 64 bit hex number - 8 chars
            String serialNumber = aikSnr.substring(9);
            X509Certificate aikCert = getValidatedCertificate(aikSubjectHash, serialNumber);
            // Retrieve the corresponding AIK public key
            PublicKey aik = aikCert.getPublicKey();
            BigInteger serialNo = aikCert.getSerialNumber();
            // Retrieve the subject public key
            X509Certificate subjCert = getValidatedCertificate(new X500Principal(subjectDN));
            PublicKey subjKey = subjCert.getPublicKey();
            //Validate the LK
            boolean valid=request.verifyRequest(aik,subjKey,nonce);
            if (!valid) {
                throw new Exception("Illegal content of LK attestation request");
            }
            //Get the actual LK
            PublicKey lk=keyInfo.getCertifiedKey();
        
            // Now construct the identity statement
            // Get attributes from a database
            String subjectId = IdentityStatement.getSubjectAltName(subjCert);
            List dbAttributes = attrDao.getAttributesFor(subjectId);
            dbAttributes.add("TPM_PROTECTED_KEYS"); dbAttributes.add("enabled");
            if (serialNo.equals(pcrInt)) {
                dbAttributes.add("TPM_PROTECTED_SOFTWARE"); dbAttributes.add("enabled");
            }
    // Create an identity statement
            IdentityStatement fass = new IdentityStatement(subjCert,lk,
                dbAttributes, issuerCrOp,validityStart,validityEnd);
            return fass;
        } catch (Exception e) {
            return e;
        }

        
    }

    // Trust in the ISes from a different COI relies on the existence of a
    // X.509 certificate issued by "me" to the foreign issuing authority.
    private void isIdentityStatementTrusted(IdentityStatement is) throws Exception {
        //PublicKey pk = is.getIssuerPublicKey();
        X500Principal issuerName = is.getIssuerDN();
        // The next statement throws an exception if not validated
        X509Certificate issuerCert = getValidatedCertificate(issuerName);
        // TODO This operation merely checks the existence of ANY certificate, not
        // necessarily an IdP certificate. Could we use the CA extension to
        // identify an IdP certificate?
        //if (issuerCert.getBasicConstraints() == -1) // No CA extension
        //    throw new GismoIdmException("Identity statement is NOT issued by an Identity Provider");
        PublicKey pk = issuerCert.getPublicKey();
        if (is.isCrossDomainIdentityStatement()) throw new GismoIdmException("Only local identity statements can be cross-signed");
        is.validateIdentityStatement(pk); // Signature and expiration correct, but is it trusted?
    }

        
    
    // Retrieve and validate certificate
    // throws exception if unknown or invalid cetifiacte
    private X509Certificate getValidatedCertificate(X500Principal dn) throws Exception {
        return certService.retrieveAndValidate(dn);
    }

    private X509Certificate getValidatedCertificate(String subjectHash, String serial) throws Exception {
        return certService.retrieveAndValidate(subjectHash,serial);
    }

}
